#include "map.hpp"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

short toShortRev(char *t)
{
    short temp = 0;
    temp |= (unsigned char)t[0];
    temp = temp << 8;
    temp |= (unsigned char)t[1];

    return temp;
}

short StrToShort(char *t)
{
    short temp = 0;
    temp = t[0] - 48;
    temp *= 10;
    temp += t[1] - 48;

    return temp;
}


//--------------------- Map ---------------------
//-------------- Private --------------
void Map::loadMap()
{
    dataBufferSize = 1201*1201;

    data = new short[dataBufferSize];

    for(uint i = 0; i < dataBufferSize; i++)
    {
        data[i] = toShortRev(&mappedMap[i*2]);
    }
}

void Map::initMap()
{
    struct stat sb;
    int fd;

    fd = open (path, O_RDONLY);
    if (fd == -1)
    {
        perror ("open");
    }

    if (fstat (fd, &sb) == -1)
    {
        perror ("fstat");
    }

    if (!S_ISREG (sb.st_mode))
    {
        fprintf (stderr, "%s is not a file\n", path);
    }

    mappedMap = (char*)mmap(0, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (mappedMap == MAP_FAILED)
    {
        perror ("mmap");
    }

    if (close (fd) == -1)
    {
        perror ("close");
    }

    loadMap();
}

void Map::initMapGPU()
{
    if(!programsLoaded)
    {
        programID[0] = LoadShaders( "MapVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );
        programID[1] = LoadShaders( "MapSphereVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );
        programsLoaded = 1;
    }

    MatrixID[0] = glGetUniformLocation(programID[0], "MVP");
    MatrixID[1] = glGetUniformLocation(programID[1], "MVP");

    CordID[0] = glGetUniformLocation(programID[0], "CORD");
    CordID[1] = glGetUniformLocation(programID[1], "CORD");

    glGenBuffers(1, &dataBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, dataBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(short) * dataBufferSize, data, GL_STATIC_DRAW);
}

void Map::getCoordinates()
{
    longitude = 0;
    latitude = 0;

    if(path[5] == 'n' || path[5] == 'N')
    {
        longitude = StrToShort(&path[6]);
    }
    else if(path[5] == 's' || path[5] == 'S')
    {
        longitude = StrToShort(&path[6]) * -1;
    }


    if(path[8] == 'e' || path[8] == 'E' )
    {
        latitude = StrToShort(&path[10]);
    }
    else if(path[8] == 'w' || path[8] == 'W')
    {
        latitude = StrToShort(&path[10]) * -1;
    }

    cord[0][0] = latitude;
    cord[0][1] = longitude;
}

//-------------- Public --------------
Map::Map(char *_path, GLuint *_elementBuffer, uint *_elementBufferSize, GLuint *_matrixBuffer)
{
    path = _path;
    elementBuffer = _elementBuffer;
    elementBufferSize = _elementBufferSize;
    matrixBuffer = _matrixBuffer;

    initMap();
    getCoordinates();
    initMapGPU();

    useProgram = 0;

    LOD = 8;
}

void Map::drawMap(mat4 VP)
{
    glUseProgram(programID[useProgram]);

    glUniformMatrix4fv(MatrixID[useProgram], 1, GL_FALSE, &VP[0][0]);
    glUniformMatrix2fv(CordID[useProgram], 1, GL_FALSE, &cord[0][0]);

    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, dataBuffer);
    glVertexAttribPointer(
        0,
        1,
        GL_SHORT,
        GL_FALSE,
        0,
        (void*)0
    );

    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, *matrixBuffer);
    glVertexAttribPointer(
        1,
        2,
        GL_SHORT,
        GL_FALSE,
        0,
        (void*)0
    );

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer[LOD]);

    glDrawElements(
        GL_TRIANGLES,
        elementBufferSize[LOD],
        GL_UNSIGNED_INT,
        (void*)0
    );

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
}

void Map::setLOD(int _LOD)
{
    if(LOD != _LOD) LOD = _LOD;
}

void Map::setView(short _view)
{
    useProgram = _view;
}

int Map::getCordX(){
    return cord[0][0];
}

int Map::getCordY(){
    return cord[0][1];
}

int *Map::getLODPointer(){
    return &LOD;
}

//--------------------- Maps Container ---------------------
//-------------- private --------------
void MapsCon::lodaMaps()
{
    DIR *dir;
    struct dirent *ent;

    int filesNum = 0;

    if ((dir = opendir (path)) != NULL)
    {

        while ((ent = readdir (dir)) != NULL)
        {
            if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue;
            filesNum++;
        }
        closedir (dir);
    }
    else
    {
        cout << "Could not open directory" << endl;
    }

    map = new Map*[filesNum];
    mapSize = filesNum;

    int i = 0;

    if ((dir = opendir (path)) != NULL)
    {
        while ((ent = readdir (dir)) != NULL)
        {
            if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue;
            map[i] = new Map((char*)(string(path) + string(ent->d_name)).c_str(),
                             elementBuffer,
                             elementBufferSize,
                             &matrixBuffer);
            cout << (char*)(string(path) + string(ent->d_name)).c_str() << endl;
            i++;
        }
        closedir (dir);
    }
    else
    {
        cout << "Could not open directory" << endl;
    }
}

void MapsCon::initMapsLODPointers(){
    for(uint i = 0; i < 360; i++){
        for(int j = 0; j < 360; j++){
            mpasLOD[i][j] = NULL;
        }
    }

    for(uint i = 0; i < mapSize; i++)
    {
        Map *m = map[i];
        mpasLOD[m->getCordX() + 180][m->getCordY() + 180] = m->getLODPointer();
    }
}

void MapsCon::createMapsLOD()
{
    short LODTable[9];
    LODTable[0] = 1;
    LODTable[1] = 2;
    LODTable[2] = 4;
    LODTable[3] = 5;
    LODTable[4] = 8;
    LODTable[5] = 10;
    LODTable[6] = 16;
    LODTable[7] = 20;
    LODTable[8] = 25;

    for(short n = 0; n < 9; n++){
        int lod = LODTable[n];
        int size = (1200)/LODTable[n];

        elementBufferSize[n] = size*size*2;

        for(short i = 0; i < size; i++)
        {
            for(short j = 0; j < size; j++)
            {
                int m = i * size * 6 + j * 6;
                int k = i * (1200 + 1) * 3 + j;
                int l = (i + 1) * (1200 + 1) * 3 + j;
                element[m] = k * lod;
                element[m+1] = (k + 1) * lod;
                element[m+2] = l * lod;
                element[m+3] = l * lod;
                element[m+4] = (l + 1) * lod;
                element[m+5] = (k + 1) * lod;
            }
        }

        glGenBuffers(1, &elementBuffer[n]);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer[n]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * elementBufferSize[n], element , GL_STATIC_DRAW);
    }
}

void MapsCon::createMapsMatrix()
{
    int size = 1201;

    matrixBufferSize = size * size * 2;

    matrix = new short[matrixBufferSize];

    for(short i = 0; i < size; i++)
    {
        for(short j = 0; j < size; j++)
        {
            int m = i * size * 2 + j * 2;
            matrix[m] = j;
            matrix[m+1] = -i;
        }
    }

    glGenBuffers(1, &matrixBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, matrixBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(short) * matrixBufferSize, matrix, GL_STATIC_DRAW);
}

void MapsCon::setMapsLOD(int _LOD)
{
    if(_LOD > -1 && _LOD < 10)
    {
        for(uint i = 0; i < mapSize; i++){
            map[i]->setLOD(_LOD);
        }
    }
}

//-------------- Public --------------
MapsCon::MapsCon(char *_path)
{
    path = _path;

    programID[0] = LoadShaders( "MapVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );
    programID[1] = LoadShaders( "SphereVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );
    programsLoaded = 1;

    element = new unsigned int[1200 * 1200 * 6];

    createMapsLOD();
    createMapsMatrix();

    lodaMaps();

    initMapsLODPointers();
}

void MapsCon::drawMaps(mat4 VP)
{
    for(uint i = 0; i < mapSize; i++)
    {
        map[i]->drawMap(VP);
    }
}

void MapsCon::changLOD(int _LOD)
{
    for(uint i = 0; i < mapSize; i++)
    {
        setMapsLOD(_LOD);
    }
}

void MapsCon::changeMapLOD(int _LOD, int x, int y)
{
    if(mpasLOD[x][y] != NULL){
        if(_LOD > -1 && _LOD < 10){
            *mpasLOD[x][y] = _LOD;
        }
    }
}

void MapsCon::setViews(short _view)
{
    for(uint i =0; i < mapSize; i++)
    {
        map[i]->setView(_view);
    }
}
